﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;

using Microsoft.Web.XmlTransform;

using MiP.Common;
using MiP.Xml.Core.StringEvaluation;

using Match = System.Text.RegularExpressions.Match;

namespace MiP.Xml.Core
{
    public class XTransformator : IXTransformator
    {
        #region Private Fields

        private const string Comma = ",";
        private const string EscapedComma = "[##EscapedComma##]";

        private readonly IStringEvaluator _evaluator;

        private readonly IXmlTransformationLogger _xmlTransformationLogger;
        private readonly ILogger _logger;

        private readonly XNamespace _xdtNamespace = "http://schemas.microsoft.com/XML-Document-Transform";
        private readonly Regex _regex = new Regex(@"([^']*)'([^']*)'([^']*)", RegexOptions.Compiled);

        #endregion

        #region Initialization

        public XTransformator(IStringEvaluator evaluator, IXmlTransformationLogger xmlTransformationLogger, ILogger logger)
        {
            if (evaluator == null)
                throw new ArgumentNullException("evaluator");
            if (xmlTransformationLogger == null)
                throw new ArgumentNullException("xmlTransformationLogger");
            if (logger == null)
                throw new ArgumentNullException("logger");

            _evaluator = evaluator;
            _xmlTransformationLogger = xmlTransformationLogger;
            _logger = logger;
        }

        #endregion

        #region Private Methods

        private void LogVariables()
        {
            _logger.Debug("-- begin variables --");

            foreach (string key in _evaluator.Variables.Keys.OrderBy(key => key))
            {
                _logger.Debug("{0} = [{1}]", key, _evaluator.Variables[key]);
            }

            _logger.Debug("-- end variables --");
        }

        private void ApplyVariables(XElement element)
        {
            IEnumerable<XNode> nodes = element.DescendantNodesAndSelf().ToList();
            // strange behaviour... when removing the .ToList() a NullReference will occur
            // in the IEnumerator.GetNext() in some cases, please leave it.

            foreach (XNode xNode in nodes)
            {
                if (xNode is XComment)
                {
                    XComment xComment = xNode as XComment;
                    if (!string.IsNullOrEmpty(xComment.Value))
                        xComment.Value = _evaluator.Evaluate(xComment.Value);
                }
                else if (xNode is XElement)
                {
                    XElement xElement = xNode as XElement;

                    // TODO: if xElement.Name.LocalName == "template" and xElement.Namespace="http://mip/xmlcmd"
                    // then remove include and insert content of template

                    if (!xElement.HasElements && !string.IsNullOrEmpty(xElement.Value))
                        xElement.Value = _evaluator.Evaluate(xElement.Value);

                    foreach (XAttribute xAttribute in xElement.Attributes())
                    {
                        string sourceString = xAttribute.Value;

                        if (string.IsNullOrEmpty(sourceString))
                            continue;

                        xAttribute.Value = _evaluator.Evaluate(sourceString);

                        if (IsConditionContainingComma(xAttribute))
                        {
                            xAttribute.Value = FixComma(xAttribute.Value);
                        }
                    }
                }
            }
        }

        private bool IsConditionContainingComma(XAttribute xAttribute)
        {
            string sourceString = xAttribute.Value;

            return xAttribute.Name.Namespace == _xdtNamespace
                   && xAttribute.Name.LocalName == "Locator"
                   && sourceString.TrimStart().StartsWith("Condition(")
                   && sourceString.Contains(Comma);
        }

        private string FixComma(string value)
        {
            // Conditions containing commas like "Condition('a,b'!='')" are failing with the message "Condition requires exactly 1 arguments".
            // A similar problem is described here, but the proposed solution doesn't work:
            // http://connect.microsoft.com/VisualStudio/feedback/details/618550/web-config-xpath-and-condition-locators-do-not-allow-commas-in-xpath-expression

            // So we escape all commas to suppress the error
            IEnumerable<string> modifiedValueParts = _regex.Matches(value)
                .Cast<Match>()
                .Select(match => match.Value.Replace(Comma, EscapedComma));
            value = string.Concat(modifiedValueParts);
            return value;
        }

        #endregion

        #region Interface Implementation: IXTransformator

        public XElement Transform(XElement source, IEnumerable<XElement> transforms, IDictionary<string, string> variables)
        {
            _evaluator.Variables = variables;

            LogVariables();

            // evaluate variables in source and transform
            ApplyVariables(source);

            // load the string to XmlDocument
            XmlDocument document = new XmlDocument();
            using (XmlReader reader = source.CreateReader())
                document.Load(reader);

            foreach (XElement transform in transforms)
            {
                ApplyVariables(transform);
                string transformXml = transform.ToString();

                // apply transformation
                XmlTransformation transformation = new XmlTransformation(transformXml, false, _xmlTransformationLogger);
                transformation.Apply(document);
            }

            // and convert XmlDocument to XElement
            XDocument result = new XDocument();
            using (XmlWriter writer = result.CreateWriter())
                document.Save(writer);

            return result.Root;
        }

        #endregion
    }
}